/***************************************************************************
 *                   (C) Copyright 2011-2017 - Marauroa                    *
 ***************************************************************************
 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

"use strict";

var marauroa = window.marauroa = window.marauroa || {};

/**
 * The IPerceptionListener interface provides methods that are called while
 * applying the perception
 */
marauroa.perceptionListener = {

	/**
	 * onAdded is called when an object is added to the world for first time or
	 * after a sync perception. Return true to stop further processing.
	 *
	 * @param object
	 *            the added object.
	 * @return true to stop further processing
	 */
	onAdded: function(object) {
		return false;
	},

	/**
	 * onModifiedAdded is called when an object is modified by adding or
	 * changing one of its attributes. Return true to stop further processing.
	 * Note that the method is called *before* modifying the object.
	 *
	 * @param object
	 *            the original object
	 * @param changes
	 *            the added and modified changes.
	 * @return true to stop further processing
	 */
	onModifiedAdded: function(object, changes) {
		return false;
	},

	/**
	 * onModifiedDeleted is called each time the object has one of its
	 * attributes removed. Return true to stop further processing. Note that the
	 * method is called *before* modifying the object.
	 *
	 * @param object
	 *            the original object
	 * @param changes
	 *            the deleted attributes.
	 * @return true to stop further processing
	 */
	onModifiedDeleted: function(object, changes) {
		return false;
	},

	/**
	 * onDeleted is called when an object is removed of the world Return true to
	 * stop further processing.
	 *
	 * @param object
	 *            the original object
	 * @return true to stop further processing
	 */
	onDeleted: function(object) {
		return false;
	},

	/**
	 * onMyRPObject is called when our rpobject avatar is processed. Return true
	 * to stop further processing.
	 *
	 * @param added
	 *            the added and modified attributes and slots
	 * @param deleted
	 *            the deleted attributes
	 * @return true to stop further processing
	 */
	onMyRPObject: function(added, deleted) {
		return false;
	},

	/**
	 * onClear is called when the whole world is going to be cleared. It happens
	 * on sync perceptions Return true to stop further processing.
	 *
	 * @return true to stop further processing
	 */
	onClear: function() {
		return false;
	},

	/**
	 * onPerceptionBegin is called when the perception is going to be applied
	 *
	 * @param type
	 *            type of the perception: SYNC or DELTA
	 * @param timestamp
	 *            the timestamp of the perception
	 */
	onPerceptionBegin: function(type, timestamp) {
	},

	/**
	 * onPerceptionBegin is called when the perception has been applied
	 *
	 * @param type
	 *            type of the perception: SYNC or DELTA
	 * @param timestamp
	 *            the timestamp of the perception
	 */
	onPerceptionEnd: function(type, timestamp) {
	},

	/**
	 * onException is called when an exception happens
	 *
	 * @param exception
	 *            the exception that happened.
	 * @param perception
	 *            the message that causes the problem
	 */
	onException: function(exception, perception) {
		console.error(exception, perception);
	}
};

marauroa.currentZone = {
	clear: function() {
		for (var i in marauroa.currentZone) {
			if (marauroa.currentZone.hasOwnProperty(i) && typeof(marauroa.currentZone[i]) !== "function") {
				marauroa.currentZone[i].destroy(marauroa.currentZone);
				delete marauroa.currentZone[i];
			}
		}
	}
};

marauroa.perceptionHandler = {

	/**
	 * Apply a perception.
	 *
	 * @param msg
	 *            the perception msg
	 */
	apply: function(msg) {
		marauroa.perceptionListener.onPerceptionBegin(msg["sync"], msg["s"]);

		// clean world on login/zone change
		if (msg["sync"]) {
			if (!marauroa.perceptionListener.onClear()) {
				marauroa.currentZone.clear();
				marauroa.currentZoneName = msg["zoneid"];
			}
		}

		// apply perception
		marauroa.perceptionHandler.applyPerceptionAddedRPObjects(msg);
		marauroa.perceptionHandler.applyPerceptionModifiedRPObjects(msg);
		marauroa.perceptionHandler.applyPerceptionDeletedRPObjects(msg);
		marauroa.perceptionHandler.applyPerceptionMyRPObject(msg);

		// done, tell marauroa.perceptionListener
		marauroa.perceptionListener.onPerceptionEnd(msg["sync"], msg["s"]);
	},

	/**
	 * This method applies perceptions addedto the Map<RPObject::ID,RPObject>
	 * passed as argument. It clears the map if this is a sync perception
	 *
	 * @param msg
	 *            the perception message
	 */
	applyPerceptionAddedRPObjects: function(msg) {
		if (msg["aO"]) {
			for (var i in msg["aO"]) {
				if (msg["aO"].hasOwnProperty(i)) {
					if (!marauroa.perceptionListener.onAdded(msg["aO"][i])) {
						var o = marauroa.rpobjectFactory.create(msg["aO"][i]["c"]);
						marauroa.perceptionHandler.addChanges(o, msg["aO"][i]);
						marauroa.currentZone[msg["aO"][i]["a"]["id"]] = o;
					}
				}
			}
		}
	},

	/**
	 * This method applys perceptions deleted to the Map<RPObject::ID,RPObject>
	 * passed as argument.
	 *
	 * @param msg
	 *            the perception message
	 */
	applyPerceptionDeletedRPObjects: function(msg) {
		if (msg["dO"]) {
			for (var i in msg["dO"]) {
				if (msg["dO"].hasOwnProperty(i)) {
					var tmp = msg["dO"][i]["a"]["id"];
					if (!marauroa.perceptionListener.onDeleted(msg["dO"][i])) {
						marauroa.currentZone[tmp].destroy(marauroa.currentZone);
						delete marauroa.currentZone[tmp];
					}
				}
			}
		}
	},

	/**
	 * This method applies perceptions modified added and modified deleted to the
	 * Map<RPObject.ID,RPObject> passed as argument.
	 *
	 * @param msg
	 *            the perception message
	 */
	applyPerceptionModifiedRPObjects: function(msg) {

		// deleted attributes
		if (msg["dA"]) {
			for (var i in msg["dA"]) {
				if (msg["dA"].hasOwnProperty(i)) {
					if (typeof(marauroa.currentZone[msg["dA"][i]["a"]["id"]]) !== "undefined") {
						var o = marauroa.currentZone[msg["dA"][i]["a"]["id"]];
						if (!marauroa.perceptionListener.onModifiedDeleted(msg["dA"][i])) {
							marauroa.perceptionHandler.deleteChanges(o, msg["dA"][i]);
						}
					}
				}
			}
		}

		// added attributes
		if (msg["aA"]) {
			for (var i in msg["aA"]) {
				if (msg["aA"].hasOwnProperty(i)) {
					if (typeof(marauroa.currentZone[msg["aA"][i]["a"]["id"]]) !== "undefined") {
						var o = marauroa.currentZone[msg["aA"][i]["a"]["id"]];
						if (!marauroa.perceptionListener.onModifiedAdded(o, msg["aA"][i])) {
							marauroa.perceptionHandler.addChanges(o, msg["aA"][i]);
						}
					}
				}
			}
		}
	},

	/**
	 * This method applys perceptions for our RPObject to the Map<RPObject::ID,RPObject>
	 * passed as argument.
	 *
	 * @param msg
	 *            the perception message
	 */
	applyPerceptionMyRPObject: function(msg) {

		if (!marauroa.perceptionListener.onMyRPObject(msg["aM"], msg["dM"])) {
			var id;
			if (typeof(msg["aM"]) !== "undefined") {
				id = msg["aM"]["a"]["id"];
			}

			if (typeof(msg["dM"]) !== "undefined") {
				id = msg["dM"]["a"]["id"];
			}

			if (typeof(id) === "undefined") {
				return;
			}

			marauroa.perceptionHandler.addMyRPObjectToWorldIfPrivate(id, msg["aM"]);
			var o = marauroa.currentZone[id];
			marauroa.me = o;
			marauroa.perceptionHandler.deleteChanges(o, msg["dM"]);
			marauroa.perceptionHandler.addChanges(o, msg["aM"]);
		}
	},

	/**
	 * adds our RPObject to the world in case it was not already added by the public perception.
	 *
	 * @param added added changes of my object
	 */
	addMyRPObjectToWorldIfPrivate: function(id, added) {
		if (typeof(marauroa.currentZone[id]) !== "undefined") {
			return;
		}
		if (!marauroa.perceptionListener.onAdded(added)) {
			if (typeof(added) === "undefined") {
				marauroa.currentZone[id] = {};
				return;
			}
			var o = marauroa.rpobjectFactory.create(added["c"]);
			marauroa.currentZone[id] = o;
			marauroa.perceptionHandler.addChanges(o, added);
		}
	},

	deleteChanges: function(object, diff) {
		if (typeof(diff) === "undefined") {
			return;
		}

		// delete attributes
		if (typeof(diff["a"]) !== "undefined") {
			for (var i in diff["a"]) {
				if (diff["a"].hasOwnProperty(i) && i !== "id" && i !== "zoneid") {
					object.unset(i);
				}
			}
		}

		// delete slots and/or their content
		if (typeof(diff["s"]) !== "undefined") {
			for (var i in diff["s"]) {
				if (!diff["s"].hasOwnProperty(i)) {
					continue;
				}
				// delete slot?
				if (marauroa.util.isEmpty(diff["s"][i])) {
					object.del(diff["s"][i]);
				} else {
					// TODO: difference between deleting an object from a slot and an attribute from a contained object
					for (var j in diff["s"][i]) {
						if (diff["s"][i].hasOwnProperty(j)) {
							object[i].del(diff["s"][i][j]["a"].id);
						}
					}
				}
			}
		}

		// delete maps and/or their content
		if (typeof(diff["m"]) !== "undefined") {
			for (var i in diff["m"]) {
				if (!diff["m"].hasOwnProperty(i)) {
					continue;
				}
				if (marauroa.util.isEmpty(diff["m"][i]["a"])) {
					object.unset(diff["m"][i]);
				} else {
					for (var j in diff["m"][i]["a"]) {
						if (diff["m"][i]["a"].hasOwnProperty(j)) {
							object.unsetMapEntry(i, j);
						}
					}
				}
			}
		}
		// TODO: links
	},

	addChanges: function(object, diff) {
		if (typeof(diff) === "undefined") {
			return;
		}
		object["_rpclass"] = diff["c"];

		// attributes
		for (var i in diff["a"]) {
			if (diff["a"].hasOwnProperty(i)) {
				if (typeof(object.set) === "undefined") {
					console.warn("Object missing set(key, value)-function", object, diff["a"]);
					object[i] = diff["a"][i];
				} else {
					object.set(i, diff["a"][i]);
				}
			}
		}

		// maps
		if (typeof(diff["m"]) !== "undefined") {
			for (var i in diff["m"]) {
				if (diff["m"].hasOwnProperty(i)) {
					if (typeof(object[i]) === "undefined") {
						object[i] = {};
					}
					for (var j in diff["m"][i]["a"]) {
						if (j !== "zoneid" && j !== "id" && diff["m"][i]["a"].hasOwnProperty(j)) {
							object.setMapEntry(i, j, diff["m"][i]["a"][j]);
						}
					}
				}
			}
		}

		// slots
		if (typeof(diff["s"]) !== "undefined") {
			for (var i in diff["s"]) {
				if (diff["s"].hasOwnProperty(i)) {
					// add slot itself, it it does not exist
					if (typeof(object[i]) === "undefined") {
						object[i] = object.createSlot(i);
					}
					// for all slot members
					for (var j in diff["s"][i]) {
						if (diff["s"][i].hasOwnProperty(j)) {
							var id = diff["s"][i][j]["a"].id;
							if (typeof(object[i].get(id)) === "undefined") {
								var newObject = marauroa.rpobjectFactory.create(diff["s"][i][j]["c"]);
								newObject._parent = object[i];
								newObject["id"] = id;
								object[i].add(newObject);
							}
							marauroa.perceptionHandler.addChanges(object[i].get(id), diff["s"][i][j])
						}
					}
				}
			}
		}

		// events
		if (typeof(diff["e"]) !== "undefined" && typeof(object.onEvent) !== "undefined") {
			for (var i in diff["e"]) {
				if (diff["e"].hasOwnProperty(i)) {
					object.onEvent(diff["e"][i]);
				}
			}
		}

		// TODO: links
	}
}
